package org.hawk.gwt.ppc.introspection.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.hawk.gwt.ppc.introspection.IntrospectionException;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaArrayTypeScope;
import org.hawk.gwt.ppc.introspection.JavaField;
import org.hawk.gwt.ppc.introspection.JavaMethod;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.JavaVariable;
import org.hawk.gwt.ppc.introspection.NoSuchTypeException;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.parser.BlockDescriptor;
import org.hawk.gwt.ppc.parser.CastExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CommaExpressionDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.ConditionalExpressionDescriptor;
import org.hawk.gwt.ppc.parser.EmptyExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ForeachExpressionDescriptor;
import org.hawk.gwt.ppc.parser.ImportsDescriptor;
import org.hawk.gwt.ppc.parser.InitializerExpressionDescriptor;
import org.hawk.gwt.ppc.parser.KeywordExpressionDescriptor;
import org.hawk.gwt.ppc.parser.LiteralExpressionDescriptor;
import org.hawk.gwt.ppc.parser.MethodCallDescriptor;
import org.hawk.gwt.ppc.parser.NameExpressionDescriptor;
import org.hawk.gwt.ppc.parser.NewOperatorExpressionDescriptor;
import org.hawk.gwt.ppc.parser.OperatorExpressionDescriptor;
import org.hawk.gwt.ppc.parser.SwitchExpressionDescriptor;
import org.hawk.gwt.ppc.parser.TernaryExpressionDescriptor;
import org.hawk.gwt.ppc.parser.TryCatchFinallyExpressionDescriptor;
import org.hawk.gwt.ppc.parser.VariableDescriptor;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Evaluates java expression to get its return type.
 * 
 * @author alex.bereznevatiy@gmail.com
 *
 */
abstract class ExpressionEvaluator<T extends ExpressionDescriptor> {
	
	private ExpressionEvaluator<?> next;
	
	private static final ExpressionEvaluator<?> ROOT = new TernaryEvaluator();
	
	private ExpressionEvaluator(ExpressionEvaluator<?> next) {
		this.next = next;
	}

	public static JavaTypeScope getReturnType(ExpressionDescriptor expression, SourcesRepository repository) throws PreprocessorException {
		JavaScopeImpl scope = null;
		CompilationUnitDescriptor unit = expression.getCompilationUnit();
		for (ClassDescriptor clazz : unit.getTopClasses()) {
			JavaTypeScope type = IntrospectionUtils.getType(clazz);
			scope = (JavaScopeImpl)type.getScope(expression.getFirstToken());
			if (scope != null) {
				break;
			}
		}
		if (scope == null) {
			throw new PreprocessorException("Internal error: scope is missing");
		}
		return ROOT.evaluate(expression, repository, scope);
	}
	
	@SuppressWarnings("unchecked")
	private JavaTypeScope evaluate(ExpressionDescriptor expression, SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
		if (isSuitable(expression)) {
			return doEvaluate((T)expression, repository, scope);
		}
		if (next != null) {
			return next.evaluate(expression, repository, scope);
		}
		throw new PreprocessorException("Unable to evaluate expression return type at " + 
				expression.getFirstToken().getLine() + " offset " + expression.getFirstToken().getOffset());
	}
	
	abstract boolean isSuitable(ExpressionDescriptor expression);
	
	abstract JavaTypeScope doEvaluate(T expression, SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException;

	/**
	 * Evaluates ternary expressions.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class TernaryEvaluator extends ExpressionEvaluator<TernaryExpressionDescriptor> {
		
		private TernaryEvaluator() {
			super(new NoTypeExpressionEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof TernaryExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(TernaryExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			// since both true and false expressions should return the same type
			// we are save to evaluate only one of them.
			return ROOT.evaluate(expression.getExpression(), repository, scope);
		} 
		
	}
	
	/**
	 * Filters out all expression that do not have any return type.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class NoTypeExpressionEvaluator extends ExpressionEvaluator<ExpressionDescriptor> {
		private NoTypeExpressionEvaluator() {
			super(new MethodCallEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof BlockDescriptor ||
					expression instanceof ConditionalExpressionDescriptor ||
					expression instanceof ForeachExpressionDescriptor ||
					expression instanceof InitializerExpressionDescriptor ||
					expression instanceof EmptyExpressionDescriptor ||
					expression instanceof KeywordExpressionDescriptor ||
					expression instanceof SwitchExpressionDescriptor ||
					expression instanceof TryCatchFinallyExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(ExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			// returning null in this case means that syntax was incorrect, 
			// but this is compiler task to show the error. 
			return null;
		}
	}
	
	/**
	 * Evaluates method calls.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class MethodCallEvaluator extends ExpressionEvaluator<MethodCallDescriptor> {
		private MethodCallEvaluator() {
			super(new NewCallEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof MethodCallDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(MethodCallDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			String name = expression.getName();
			
			JavaScopeImpl target = scope;
			if (expression.getTarget() != null) { 
				target = (JavaScopeImpl) ROOT.evaluate(expression.getTarget(), repository, scope);
			}
			List<JavaTypeScope> arguments = new ArrayList<JavaTypeScope>(expression.getArguments().size());
			
			for (ExpressionDescriptor arg : expression.getArguments()) {
				arguments.add(ROOT.evaluate(arg, repository, scope));
			}
			if (target == null) {
				throw new IntrospectionException("Internal error: Unable to resolve target");
			}
			JavaMethod method = target.getBestMatchMethod(name, arguments);
			
			if (method == null && expression.getTarget() == null) {
				method = resolveStaticImport(expression, name, arguments, repository);
			}
			
			if (method == null) {
				throw new PreprocessorException("Unable to evaluate return type of expression: " +
						"no such method: " + name + arguments);
			}
			
			return method.getReturnType();
		}

		private JavaMethod resolveStaticImport(MethodCallDescriptor expression,
				String name, List<JavaTypeScope> arguments, SourcesRepository repository) throws PreprocessorException {
			ImportsDescriptor imports = expression.getCompilationUnit().getImports();
			List<String> results = imports.resolveMethod(name);
			for (String fullName : results) {
				String typeName = Name.getParentJavaName(fullName);
				JavaTypeScope scope = IntrospectionUtils.resolveType(typeName, expression.getCompilationUnit(), repository);
				JavaMethod result = scope.getBestMatchMethod(name, arguments);
				if (result != null) {
					return result;
				}
			}
			return null;
		}
	}
	
	/**
	 * Evaluates new expressions.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class NewCallEvaluator extends ExpressionEvaluator<NewOperatorExpressionDescriptor> {
		private NewCallEvaluator() {
			super(new CastEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof NewOperatorExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(NewOperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			return IntrospectionUtils.resolveType(expression.getType(), expression.getCompilationUnit(), repository);
		}
	}
	
	/**
	 * Evaluates cast expressions.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class CastEvaluator extends ExpressionEvaluator<CastExpressionDescriptor> {
		private CastEvaluator() {
			super(new CommaEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof CastExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(CastExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			return IntrospectionUtils.resolveType(expression.getType(), expression.getCompilationUnit(), repository);
		}
	}
	
	/**
	 * Evaluates comma expressions.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class CommaEvaluator extends ExpressionEvaluator<CommaExpressionDescriptor> {
		private CommaEvaluator() {
			super(new LiteralEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof CommaExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(CommaExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			return null;// comma expression has no return value
		}
	}
	
	/**
	 * Evaluates java literals.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class LiteralEvaluator extends ExpressionEvaluator<LiteralExpressionDescriptor> {
		private LiteralEvaluator() {
			super(new NameEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof LiteralExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(LiteralExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			return IntrospectionUtils.getType(expression.getType());
		}
	}
	
	/**
	 * Evaluates java names.
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class NameEvaluator extends ExpressionEvaluator<NameExpressionDescriptor> {
		
		
		private NameEvaluator() {
			super(new VariableEvaluator());
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof NameExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(NameExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			String name = expression.getName();
			JavaVariable var = scope.getVariable(name, expression.getFirstToken());
			if (var != null) {
				return var.getTypeDescriptor();
			}
			try {
				return scope.resolveType(name, expression.getCompilationUnit());
			} catch (NoSuchTypeException ex) {
				return null;
			}
		}
	}
	
	private static class VariableEvaluator extends ExpressionEvaluator<VariableDescriptor> {
		private VariableEvaluator() {
			super(new OperatorEvaluator());			
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof VariableDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(VariableDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			String type = Name.getRawName(expression.getType());
			return IntrospectionUtils.resolveType(type, expression.getCompilationUnit(), repository);
		}
	}
	
	/**
	 * Evaluates java operators (e.g. dot or +).
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class OperatorEvaluator extends ExpressionEvaluator<OperatorExpressionDescriptor> {
		@SuppressWarnings("serial")
		private static Map<String, OperatorsBuilder> builders = Collections.unmodifiableMap(
				new HashMap<String, ExpressionEvaluator.OperatorsBuilder>() {
					{
						put(DotOperatorEvaluator.OPERATOR, new DotOperatorEvaluator());
						put("+", new OtherOperatorsEvaluator());
						put("-", new OtherOperatorsEvaluator());
						put("*", new OtherOperatorsEvaluator());
						put("/", new OtherOperatorsEvaluator());
						put("%", new OtherOperatorsEvaluator());
						put("=", new EqualsOperatorsEvaluator());
						put(">>", new OtherOperatorsEvaluator());
						put("<<", new OtherOperatorsEvaluator());
						put(">>>", new OtherOperatorsEvaluator());
						put(">", new BooleanOperatorsEvaluator());
						put("<", new BooleanOperatorsEvaluator());
						put(">=", new BooleanOperatorsEvaluator());
						put("<=", new BooleanOperatorsEvaluator());
						put("==", new BooleanOperatorsEvaluator());
						put("!=", new BooleanOperatorsEvaluator());
						put("&", new OtherOperatorsEvaluator());
						put("^", new OtherOperatorsEvaluator());
						put("|", new OtherOperatorsEvaluator());
						put("instanceof", new BooleanOperatorsEvaluator());
						put("&&", new BooleanOperatorsEvaluator());
						put("||", new BooleanOperatorsEvaluator());
						put("*=", new EqualsOperatorsEvaluator());
						put("/=", new EqualsOperatorsEvaluator());
						put("+=", new EqualsOperatorsEvaluator());
						put("-=", new EqualsOperatorsEvaluator());
						put("%=", new EqualsOperatorsEvaluator());
						put("<<=", new EqualsOperatorsEvaluator());
						put(">>=", new EqualsOperatorsEvaluator());
						put(">>>=", new EqualsOperatorsEvaluator());
						put("&=", new EqualsOperatorsEvaluator());
						put("|=", new EqualsOperatorsEvaluator());
						put("^=", new EqualsOperatorsEvaluator());
						put("[", new ArrayOperatorsEvaluator());
						put("++", new OtherOperatorsEvaluator());
						put("--", new OtherOperatorsEvaluator());
						put("~", new OtherOperatorsEvaluator());
						put("!", new BooleanOperatorsEvaluator());
					}
		});
		
		private OperatorEvaluator() {
			super(null);
		}

		@Override
		boolean isSuitable(ExpressionDescriptor expression) {
			return expression instanceof OperatorExpressionDescriptor;
		}

		@Override
		JavaTypeScope doEvaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			OperatorsBuilder builder = builders.get(expression.getOperator());
			if (builder == null) {
				throw new PreprocessorException("Unknown operator: " + expression.getOperator());
			}
			return builder.evaluate(expression, repository, scope);
		}
	}
	
	/**
	 * Common interface for all operator evaluators.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static interface OperatorsBuilder {

		JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException;
		
	}

	/**
	 * Evaluates dot operator (either property access or 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class DotOperatorEvaluator implements OperatorsBuilder {

		static final String OPERATOR = ".";

		public JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope)
				throws PreprocessorException {
			
			ExpressionDescriptor left = expression;
			LinkedList<OperatorExpressionDescriptor> expressions = new LinkedList<OperatorExpressionDescriptor>();
			do {
				OperatorExpressionDescriptor operator = (OperatorExpressionDescriptor)left;
				expressions.addFirst(operator);
				left = operator.getLeftOperand();
			} while (left instanceof OperatorExpressionDescriptor &&
				((OperatorExpressionDescriptor)left).getOperator().equals(OPERATOR));
			
			JavaTypeScope type = ROOT.evaluate(left, repository, scope);
			if (type == null) {
				return evaluateTypeName(left, expressions, repository, scope);
			}
			return evaluateProperty(type, expressions, repository, scope);
		}

		private JavaTypeScope evaluateTypeName(ExpressionDescriptor left,
				LinkedList<OperatorExpressionDescriptor> expressions,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			if (!(left instanceof NameExpressionDescriptor)) {
				throw new PreprocessorException("Internal error: bad AST");
			}
			StringBuilder name = new StringBuilder();
			name.append(((NameExpressionDescriptor)left).getName());
			
			try {
				JavaTypeScope type = IntrospectionUtils.resolveType(name.toString(), left.getCompilationUnit(), repository);
				return evaluateProperty(type, expressions, repository, scope);
			} catch (NoSuchTypeException ex) {// ignore
			}
			
			JavaTypeScope type = null;
			while (!expressions.isEmpty()) {
				OperatorExpressionDescriptor operator = expressions.removeFirst();
				if (!(operator.getRightOperand() instanceof NameExpressionDescriptor)) {
					throw new PreprocessorException("Internal error: bad AST");
				}
				name.append('.').append(((NameExpressionDescriptor)operator.getRightOperand()).getName());
				try {
					type = IntrospectionUtils.resolveType(name.toString(), left.getCompilationUnit(), repository);
				} catch (NoSuchTypeException ex) {// ignore
					if (type != null) {
						expressions.addFirst(operator);
						return evaluateProperty(type, expressions, repository, scope);
					}
				}
			}
			
			if (type != null) {
				return type;
			}
			
			throw new NoSuchTypeException("Internal error: unable to find a type: " + name);
		}

		private JavaTypeScope evaluateProperty(JavaTypeScope type,
				LinkedList<OperatorExpressionDescriptor> expressions,
				SourcesRepository repository, JavaScopeImpl scope) throws PreprocessorException {
			
			for (OperatorExpressionDescriptor expression : expressions) {
				if (!(expression.getRightOperand() instanceof NameExpressionDescriptor)) {
					throw new PreprocessorException("Internal error: bad AST");
				}
				NameExpressionDescriptor name = (NameExpressionDescriptor) expression.getRightOperand();
				String typeName = type.getName();
				JavaField field = type.getField(name.getName());
				if (field == null) {
					throw new PreprocessorException("No such property " + typeName + "." + name.getName());
				}
				type = field.getTypeDescriptor();
				if (type == null) {
					throw new PreprocessorException("No such property " + typeName + "." + name.getName());
				}
			}
			return type;
		}

	}
	
	/**
	 * Always returns {@link Boolean} type.
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class BooleanOperatorsEvaluator implements OperatorsBuilder {

		public JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope)
				throws PreprocessorException {
			return PrimitiveTypeScopes.BOOLEAN;
		}
	}
	
	/**
	 * Provides return value of other non-boolean binary operators.
	 * Logic is following: 
	 * 1) if one of the operands returns String or object - it will define the result.
	 * 2) if one of the operands return double - double will be the result
	 * 3) if one of the operands return float - float will be the result
	 * 3) if one of the operands returns long - long will be the result
	 * 4) otherwise int will be the result.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class OtherOperatorsEvaluator implements OperatorsBuilder {

		public JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope)
				throws PreprocessorException {
			JavaTypeScope left = null;
			JavaTypeScope right = null;
			if (expression.getLeftOperand() != null) {
				left = ROOT.evaluate(expression.getLeftOperand(), repository, scope);
			}
			if (expression.getRightOperand() != null) {
				right = ROOT.evaluate(expression.getRightOperand(), repository, scope);
			}
			if ((left != null && !left.isPrimitive() && !left.isPrimitiveWrapper()) ||
					(right != null && !right.isPrimitive() && !right.isPrimitiveWrapper())) {
				return IntrospectionUtils.getType(String.class);
			}
			if (is(left, "double", "java.lang.Double") || is(right, "double", "java.lang.Double")) {
				return PrimitiveTypeScopes.DOUBLE; 
			}
			if (is(left, "float", "java.lang.Float") || is(right, "float", "java.lang.Float")) {
				return PrimitiveTypeScopes.FLOAT;
			}
			if (is(left, "long", "java.lang.Long") || is(right, "long", "java.lang.Long")) {
				return PrimitiveTypeScopes.FLOAT;
			}
			return PrimitiveTypeScopes.INT;
		}

		private boolean is(JavaTypeScope scope, String...names) {
			if (scope == null) {
				return false;
			}
			String name = scope.getName();
			for (String check : names) {
				if (name.equals(check)) {
					return true;
				}
			}
			return false;
		}
		
	}
	
	/**
	 * Evaluates equals operators
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class EqualsOperatorsEvaluator implements OperatorsBuilder {

		public JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope)
				throws PreprocessorException {
			return ROOT.evaluate(expression.getLeftOperand(), repository, scope);
		}
		
	}
	
	/**
	 * Returns array element type.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 *
	 */
	private static class ArrayOperatorsEvaluator implements OperatorsBuilder {

		public JavaTypeScope evaluate(OperatorExpressionDescriptor expression,
				SourcesRepository repository, JavaScopeImpl scope)
				throws PreprocessorException {
			JavaTypeScope array = ROOT.evaluate(expression.getLeftOperand(), repository, scope);
			if (array instanceof JavaArrayTypeScope) {
				return ((JavaArrayTypeScope)array).getComponentType();
			}
			throw new PreprocessorException(array.getName() + " is not an array");
		}
		
	}
}
